Анализ данных о сердечно-сосудистых заболеваниях (поиск инсайтов, составление рекомендаций стейкхолдерам)

Author
Affiliation

Алексей Якиманский

Netology, DSU-73

Abstract

В данном исследовании проводится комплексный анализ данных о сердечно-сосудистых заболеваниях с целью выявления ключевых факторов риска и построения предиктивных моделей. Анализ включает исследовательский анализ данных, разработку и сравнение моделей машинного обучения для прогнозирования наличия сердечно-сосудистых заболеваний.

Введение

Сердечно-сосудистые заболевания являются основной причиной смертности во многих странах мира. Раннее выявление факторов риска и своевременная профилактика играют ключевую роль в снижении заболеваемости и смертности.

Цель исследования

Основной целью данного исследования является анализ факторов риска сердечно-сосудистых заболеваний на основе данных медицинских обследований и построение предиктивных моделей для оценки вероятности наличия заболевания.

Задачи исследования

  1. Провести исследовательский анализ данных для выявления ключевых закономерностей
  2. Выполнить очистку и предобработку данных
  3. Построить и оценить предиктивные модели
  4. Сформулировать практические рекомендации для заинтересованных лиц

Основные стейкхолдеры

1. Медицинская лаборатория

Приоритеты:

  • Повышение точности диагностики сердечно-сосудистых заболеваний
  • Оптимизация скрининговых программ
  • Снижение затрат на обработку данных
  • Улучшение качества предоставляемых услуг

Задачи:

  • Внедрение предиктивных моделей в рутинную практику
  • Обучение персонала работе с ML-инструментами
  • Интеграция моделей в существующие лабораторные системы
  • Мониторинг эффективности внедренных решений

2. Врачи-кардиологи и терапевты

Приоритеты:

  • Получение точных инструментов для оценки риска пациентов
  • Сокращение времени на принятие клинических решений
  • Повышение качества лечения и профилактики
  • Снижение пропускной способности высокорисковых пациентов

Задачи:

  • Использование предиктивных моделей в клинической практике
  • Интерпретация результатов ML-моделей для пациентов
  • Адапация рекомендаций под индивидуальные особенности пациентов
  • Обеспечение этического использования алгоритмов

3. Пациенты

Приоритеты:

  • Своевременное выявление рисков сердечно-сосудистых заболеваний
  • Получение персонализированных рекомендаций
  • Повышение качества жизни и здоровья
  • Снижение тревожности относительно состояния здоровья

Задачи:

  • Прохождение регулярных обследований
  • Следование рекомендациям по изменению образа жизни
  • Активное участие в программах мониторинга здоровья
  • Соблюдение предписанного лечения

4. Система здравоохранения

Приоритеты:

  • Снижение общей заболеваемости и смертности от ССЗ
  • Оптимизация распределения медицинских ресурсов
  • Повышение эффективности профилактических программ
  • Снижение экономических затрат на лечение ССЗ

Задачи:

  • Разработка и внедрение национальных скрининговых программ
  • Создание реестров пациентов с высоким риском
  • Обеспечение доступности качественной медицинской помощи
  • Мониторинг популяционных показателей здоровья

5. Страховые компании

Приоритеты:

  • Снижение выплат по дорогостоящим случаям лечения ССЗ
  • Оптимизация тарифов страховых продуктов
  • Повышение удержания клиентов через профилактические программы
  • Точный расчет актуарных рисков

Задачи:

  • Разработка программ превентивной медицины
  • Интеграция моделей оценки рисков в андеррайтинг
  • Создание стимулов для здорового образа жизни клиентов
  • Мониторинг медицинских расходов клиентов

6. Исследователи и академическое сообщество

Приоритеты:

  • Получение новых научных знаний о факторах риска ССЗ
  • Валидация методологий машинного обучения в медицине
  • Публикация результатов в рецензируемых журналах
  • Развитие междисциплинарного сотрудничества

Задачи:

  • Проведение дополнительных исследований на расширенных данных
  • Валидация моделей на независимых выборках
  • Разработка новых методологий анализа
  • Подготовка научных публикаций и презентаций

7. Разработчики медицинских технологий

Приоритеты:

  • Создание коммерчески жизнеспособных продуктов
  • Обеспечение соответствия регуляторным требованиям
  • Масштабирование решений для широкого использования
  • Поддержание конкурентоспособности на рынке

Задачи:

  • Разработка пользовательских интерфейсов для клиницистов
  • Интеграция с существующими медицинскими системами (HIS/EMR)
  • Обеспечение безопасности и конфиденциальности данных
  • Проведение клинических испытаний и сертификация

Ключевые метрики успеха для стейкхолдеров

Медицинская лаборатория:

  • Снижение времени обработки анализов
  • Повышение точности прогнозирования
  • Увеличение количества обслуживаемых пациентов

Врачи:

  • Сокращение времени на принятие решений
  • Повышение выявляемости заболеваний на ранних стадиях
  • Удовлетворенность пациентов

Пациенты:

  • Повышение приверженности лечению
  • Снижение прогрессирования заболеваний
  • Улучшение качества жизни

Система здравоохранения:

  • Снижение госпитализаций по поводу ССЗ
  • Экономическая эффективность
  • Покрытие скринингом большей части целевой популяции

Обзор данных

В исследовании используется датасет Cardiovascular Disease Dataset, содержащий информацию о 70 000 пациентах. Данные предоставлены медицинской лабораторией и включают 11 признаков и целевую переменную наличия сердечно-сосудистого заболевания.

Описание признаков

  • age - возраст в днях
  • gender - пол (1 - женщина, 2 - мужчина)
  • height - рост в см
  • weight - вес в кг
  • ap_hi - систолическое артериальное давление
  • ap_lo - диастолическое артериальное давление
  • cholesterol - уровень холестерина (1: нормальный, 2: выше нормы, 3: высокий)
  • gluc - уровень глюкозы (1: нормальный, 2: выше нормы, 3: высокий)
  • smoke - курение (0: нет, 1: да)
  • alco - употребление алкоголя (0: нет, 1: да)
  • active - физическая активность (0: нет, 1: да)
  • cardio - наличие сердечно-сосудистого заболевания (0: нет, 1: да)

Методология

Подходы к анализу

Исследование будет проводиться в несколько этапов:

  1. Исследовательский анализ данных (EDA): анализ распределений, выявление выбросов, изучение взаимосвязей
  2. Предобработка данных: очистка, нормализация, создание новых признаков
  3. Моделирование: построение и сравнение моделей машинного обучения
  4. Интерпретация результатов: анализ важности признаков и формулирование выводов

Инструменты анализа

  • Python 3.12+ с научными библиотеками pandas, numpy, matplotlib, seaborn
  • scikit-learn для построения моделей машинного обучения
  • Quarto для генерации отчета

Результаты EDA

Настройка окружения

Для начала импортируем необходимые библиотеки и настроим параметры визуализации.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings

from pathlib import Path
from great_tables import GT

from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (accuracy_score, precision_score, recall_score, 
                           f1_score, roc_auc_score, roc_curve, 
                           confusion_matrix, classification_report)

Настроим параметры отображения в ноутбуке

np.random.seed(31337)

warnings.filterwarnings('ignore')

# Настройки для визуализаций
plt.style.use('seaborn-v0_8-whitegrid')

# Монохромная палитра с красными акцентами
colors = ['#808080', '#606060', '#404040', '#FF6B6B', '#CC5555']
sns.set_palette(colors)

plt.rcParams['font.size'] = 11
plt.rcParams['figure.titlesize'] = 14
plt.rcParams['axes.titlesize'] = 12
plt.rcParams['axes.labelsize'] = 11

Вывод: Окружение настроено, необходимые библиотеки импортированы, параметры визуализации заданы.

Загрузка данных

Загрузим набор данных и выведем основную информацию о его размере.

DF_CSV_PATH = 'data/cardio_train.csv'
df = pd.read_csv(DF_CSV_PATH, sep=';')

Вывод: Данные успешно загружены. Исходный файл прочитан корректно.

Предварительный просмотр

Ознакомимся со структурой данных предоставленного датасета.

print(f"Размер датасета: {df.shape}")
Размер датасета: (70000, 13)

Вывод: Исходный набор данных содержит 70 000 записей и 13 столбцов.

GT(df.head())
Первые 5 строк датасета
id age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
0 18393 2 168 62.0 110 80 1 1 0 0 1 0
1 20228 1 156 85.0 140 90 3 1 0 0 1 1
2 18857 1 165 64.0 130 70 3 1 0 0 0 1
3 17623 2 169 82.0 150 100 1 1 0 0 1 1
4 17474 1 156 56.0 100 60 1 1 0 0 0 0

Вывод: Структура данных соответствует описанию: присутствуют ID, возраст, пол, антропометрические данные и показатели здоровья.

Типы данных

Проверим типы данных каждого признака, чтобы убедиться в их корректности.

types_df = df.dtypes.reset_index()
types_df.columns = ["Признак", "Тип данных"]
GT(types_df)
Типы данных в датасете
Признак Тип данных
id int64
age int64
gender int64
height int64
weight float64
ap_hi int64
ap_lo int64
cholesterol int64
gluc int64
smoke int64
alco int64
active int64
cardio int64

Вывод: Типы данных интерпретированы корректно (целые и вещественные числа), дополнительных преобразований типов на данном этапе не требуется.

Описательная статистика

Рассмотрим основные статистические характеристики числовых признаков.

stats_df = df.describe().reset_index()
GT(stats_df)
Описательная статистика числовых признаков
index id age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
count 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0 70000.0
mean 49972.4199 19468.865814285713 1.3495714285714286 164.35922857142856 74.20569 128.8172857142857 96.63041428571428 1.3668714285714285 1.226457142857143 0.08812857142857143 0.053771428571428574 0.8037285714285715 0.4997
std 28851.30232317292 2467.2516672414013 0.47683801558286387 8.210126364538038 14.395756678511379 154.01141945609137 188.47253029639026 0.680250348699381 0.572270276613845 0.28348381676993517 0.2255677036041049 0.3971790635049283 0.5000034814661862
min 0.0 10798.0 1.0 55.0 10.0 -150.0 -70.0 1.0 1.0 0.0 0.0 0.0 0.0
25% 25006.75 17664.0 1.0 159.0 65.0 120.0 80.0 1.0 1.0 0.0 0.0 1.0 0.0
50% 50001.5 19703.0 1.0 165.0 72.0 120.0 80.0 1.0 1.0 0.0 0.0 1.0 0.0
75% 74889.25 21327.0 2.0 170.0 82.0 140.0 90.0 2.0 1.0 0.0 0.0 1.0 1.0
max 99999.0 23713.0 2.0 250.0 200.0 16020.0 11000.0 3.0 3.0 1.0 1.0 1.0 1.0

Вывод: Описательная статистика указывает на наличие аномальных значений (выбросов) в полях роста, веса и артериального давления, которые потребуют очистки.

Проверка на пропуски

Важным этапом является проверка данных на наличие пропущенных значений.

# Проверка пропусков
missing_values = df.isnull().sum().reset_index()
missing_values.columns = ["Признак", "Количество пропусков"]

if missing_values["Количество пропусков"].sum() == 0:
    print("Пропусков не обнаружено")
else:
    GT(missing_values[missing_values["Количество пропусков"] > 0])
Пропусков не обнаружено

Вывод: Пропущенные значения в датасете не обнаружены.

Проверка дубликатов

Проверим наличие полных дубликатов записей, которые могут исказить результаты анализа, и удалим их при наличии.

# Проверка дубликатов
duplicates = df.duplicated().sum()
print(f"Количество полных дубликатов: {duplicates}")

# Удаление дубликатов если есть
if duplicates > 0:
    df = df.drop_duplicates()
    print(f"После удаления дубликатов размер: {df.shape}")
Количество полных дубликатов: 0

Вывод: Проверка на дубликаты выполнена. Уникальность записей подтверждена (или восстановлена).

Анализ выбросов

Используем диаграммы размаха (boxplot) для выявления аномальных значений в числовых признаках.

Для удобства анализа преобразуем возраст из дней в годы во временной колонке для визуализации (до очистки).

df['age_years_raw'] = df['age'] / 365.25

Выбросы: Возраст

plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='age_years_raw', color='#808080')
plt.title('Box Plot: Возраст', fontsize=14)
plt.xlabel('Лет')
plt.show()

Box plot для возраста

Box plot для возраста

Вывод: Распределение возраста не содержит явных аномалий, диапазон значений реалистичен.

Выбросы: Рост

plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='height', color='#808080')
plt.title('Box Plot: Рост', fontsize=14)
plt.xlabel('см')
plt.show()

Box plot для роста

Box plot для роста

Вывод: Присутствуют выбросы в росте (слишком низкие и высокие значения), которые вероятно являются ошибками ввода.

Выбросы: Вес

plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='weight', color='#808080')
plt.title('Box Plot: Вес', fontsize=14)
plt.xlabel('кг')
plt.show()

Box plot для веса

Box plot для веса

Вывод: Аналогично росту, вес содержит подозрительные экстремальные значения.

Выбросы: Систолическое давление

plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='ap_hi', color='#808080')
plt.title('Box Plot: Систолическое давление', fontsize=14)
plt.xlabel('мм рт.ст.')
plt.show()

Box plot для систолического давления

Box plot для систолического давления

Вывод: Данные по давлению сильно “зашумлены” экстремальными выбросами, что подтверждает необходимость жесткой фильтрации.

Выбросы: Диастолическое давление

plt.figure(figsize=(8, 4))
sns.boxplot(data=df, x='ap_lo', color='#808080')
plt.title('Box Plot: Диастолическое давление', fontsize=14)
plt.xlabel('мм рт.ст.')
plt.show()

Box plot для диастолического давления

Box plot для диастолического давления

Вывод: Диастолическое давление также требует очистки от нереалистичных значений.

Количественная оценка выбросов по методу межквартильного размаха (IQR).

numeric_features = ['age_years_raw', 'height', 'weight', 'ap_hi', 'ap_lo']
outliers_data = []

for feature in numeric_features:
    Q1 = df[feature].quantile(0.25)
    Q3 = df[feature].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers_count = len(df[(df[feature] < lower_bound) | (df[feature] > upper_bound)])
    outliers_data.append({
        'Признак': feature,
        'Количество выбросов': outliers_count,
        'Процент': f"{outliers_count/len(df)*100:.1f}%"
    })

GT(pd.DataFrame(outliers_data))
Статистика по выбросам
Признак Количество выбросов Процент
age_years_raw 4 0.0%
height 519 0.7%
weight 1819 2.6%
ap_hi 1435 2.1%
ap_lo 4632 6.6%

Вывод: Статистика IQR подтверждает, что наибольшее количество выбросов содержится в показателях давления, что критично для корректного моделирования.

Очистка данных

На основе EDA проведем очистку данных от аномальных и нереалистичных значений.

Инициализация

Создадим копию датафрейма для очистки.

df_clean = df.copy()
# Удаляем временную колонку, если она осталась
if 'age_years_raw' in df_clean.columns:
    df_clean = df_clean.drop('age_years_raw', axis=1)

print(f"Исходный размер датасета: {df_clean.shape}")
Исходный размер датасета: (70000, 13)

Вывод: Подготовка к очистке выполнена, работаем с копией данных для безопасности.

Очистка артериального давления

Фильтрация нереалистичных значений давления. Используем следующие критерии: - Систолическое: 70-250 мм рт.ст. - Диастолическое: 40-150 мм рт.ст. - Систолическое должно быть выше диастолического.

before_pressure = len(df_clean)
df_clean = df_clean[
    (df_clean['ap_hi'] >= 70) & (df_clean['ap_hi'] <= 250) &
    (df_clean['ap_lo'] >= 40) & (df_clean['ap_lo'] <= 150) &
    (df_clean['ap_hi'] > df_clean['ap_lo'])
]
after_pressure = len(df_clean)
print(f"Удалено записей с нереалистичным давлением: {before_pressure - after_pressure}")
Удалено записей с нереалистичным давлением: 1334

Вывод: Фильтрация давления удалила наиболее грубые ошибки, существенно повысив качество данных.

Очистка антропометрических данных

Фильтрация по росту и весу: - Рост: 100-220 см - Вес: 30-250 кг

before_anthro = len(df_clean)
df_clean = df_clean[
    (df_clean['height'] >= 100) & (df_clean['height'] <= 220) &
    (df_clean['weight'] >= 30) & (df_clean['weight'] <= 250)
]
after_anthro = len(df_clean)
print(f"Удалено записей с нереалистичным ростом/весом: {before_anthro - after_anthro}")
Удалено записей с нереалистичным ростом/весом: 33

Вывод: Исключены записи с физиологически невозможными сочетаниями роста и веса.

Очистка и преобразование возраста

Оставляем пациентов от 18 до 100 лет и создаем признак age_years для анализа.

# Создаем возраст в годах
df_clean['age_years'] = df_clean['age'] / 365.25

before_age = len(df_clean)
df_clean = df_clean[
    (df_clean['age_years'] >= 18) & (df_clean['age_years'] <= 100)
]
after_age = len(df_clean)
print(f"Удалено записей с нереалистичным возрастом: {before_age - after_age}")
Удалено записей с нереалистичным возрастом: 0

Вывод: Возрастной фильтр отработал, данные по возрасту были достаточно чистыми. Возраст успешно сконвертирован в годы.

Расчет BMI

Рассчитаем индекс массы тела (BMI) для дальнейшего анализа.

df_clean['bmi'] = df_clean['weight'] / (df_clean['height'] / 100) ** 2
print(f"Итоговый размер после очистки: {df_clean.shape}")
Итоговый размер после очистки: (68633, 15)

Вывод: Рассчитан BMI, который является интегральным показателем, часто более информативным, чем вес и рост по отдельности.

Статистика очищенного датасета:

clean_stats = df_clean[['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']].describe().reset_index()
GT(clean_stats)
Статистика после очистки
index age_years height weight ap_hi ap_lo bmi
count 68633.0 68633.0 68633.0 68633.0 68633.0 68633.0
mean 53.291214957737346 164.3946206635292 74.11911034050677 126.67120772806085 81.30172074657963 27.473124357736904
std 6.757253833720525 7.977184812426184 14.307359581664704 16.681362962533587 9.422616258222744 5.351510180908495
min 29.56331279945243 100.0 30.0 70.0 40.0 10.726643598615919
25% 48.34496919917864 159.0 65.0 120.0 80.0 23.875114784205696
50% 53.93839835728953 165.0 72.0 120.0 80.0 26.346494034400994
75% 58.38193018480493 170.0 82.0 140.0 90.0 30.119375573921033
max 64.92265571526352 207.0 200.0 240.0 150.0 152.55177514792896

Вывод: После очистки статистики (min/max/std) выглядят правдоподобно и пригодны для анализа.

Анализ категориальных признаков (структура)

Посмотрим на уникальные значения в категориальных переменных для понимания их структуры (на очищенных данных).

categorical_cols = ['gender', 'cholesterol', 'gluc', 'smoke', 'alco', 'active', 'cardio']
unique_data = []

for col in categorical_cols:
    unique_vals = sorted(df_clean[col].unique())
    unique_data.append({"Признак": col, "Уникальные значения": str(unique_vals)})

GT(pd.DataFrame(unique_data))
Уникальные значения категориальных признаков
Признак Уникальные значения
gender [np.int64(1), np.int64(2)]
cholesterol [np.int64(1), np.int64(2), np.int64(3)]
gluc [np.int64(1), np.int64(2), np.int64(3)]
smoke [np.int64(0), np.int64(1)]
alco [np.int64(0), np.int64(1)]
active [np.int64(0), np.int64(1)]
cardio [np.int64(0), np.int64(1)]

Вывод: Значения категориальных признаков соответствуют ожидаемым и не содержат неявных дубликатов или ошибок ввода.

Распределение целевой переменной

Проанализируем сбалансированность классов целевой переменной cardio в очищенном датасете.

plt.figure(figsize=(8, 6))
ax = sns.countplot(data=df_clean, x='cardio', palette=['#808080', '#FF6B6B'])
plt.title('Распределение наличия сердечно-сосудистых заболеваний', fontsize=14, pad=20)
plt.xlabel('Наличие заболевания (0 - нет, 1 - да)', fontsize=12)
plt.ylabel('Количество пациентов', fontsize=12)

# Добавление процентов
total = len(df_clean)
for p in ax.patches:
    percentage = f'{100 * p.get_height() / total:.1f}%'
    ax.annotate(percentage, (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='bottom', fontsize=11)

plt.tight_layout()
plt.show()

График распределения наличия сердечно-сосудистых заболеваний

График распределения наличия сердечно-сосудистых заболеваний

Детальная статистика распределения целевой переменной:

target_stats = df_clean['cardio'].value_counts().reset_index()
target_stats.columns = ['Cardio', 'Count']
target_stats['Percentage'] = (target_stats['Count'] / total * 100).round(1).astype(str) + '%'
GT(target_stats)
Статистика распределения целевой переменной
Cardio Count Percentage
0 34679 50.5%
1 33954 49.5%

Вывод: Классы сбалансированы (~50% на 50%), что позволяет использовать accuracy как одну из метрик и не требует применения техник балансировки (SMOTE и др.).

Распределения числовых признаков (очищенные данные)

Возраст

Рассмотрим распределение возраста пациентов.

plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='age_years', bins=30, color='#808080', alpha=0.7)
plt.title('Распределение возраста (годы)', fontsize=14)
plt.xlabel('Возраст, лет')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()

Гистограмма распределения возраста

Гистограмма распределения возраста

Вывод: Основная масса пациентов находится в возрасте от 40 до 65 лет, что соответствует группе риска ССЗ.

Рост

Анализ распределения роста пациентов.

plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='height', bins=30, color='#808080', alpha=0.7)
plt.title('Распределение роста', fontsize=14)
plt.xlabel('Рост, см')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()

Гистограмма распределения роста

Гистограмма распределения роста

Вывод: Распределение роста близко к нормальному.

Вес

Анализ распределения веса пациентов.

plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='weight', bins=30, color='#808080', alpha=0.7)
plt.title('Распределение веса', fontsize=14)
plt.xlabel('Вес, кг')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()

Гистограмма распределения веса

Гистограмма распределения веса

Вывод: Распределение веса имеет “тяжелый” правый хвост, указывающий на наличие пациентов с значительным избыточным весом.

Систолическое давление

Распределение верхнего (систолического) артериального давления.

plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='ap_hi', bins=30, color='#808080', alpha=0.7)
plt.title('Систолическое артериальное давление', fontsize=14)
plt.xlabel('Давление, мм рт.ст.')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()

Гистограмма систолического давления

Гистограмма систолического давления

Вывод: Данные по давлению теперь находятся в реалистичном диапазоне.

Диастолическое давление

Распределение нижнего (диастолического) артериального давления.

plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='ap_lo', bins=30, color='#808080', alpha=0.7)
plt.title('Диастолическое артериальное давление', fontsize=14)
plt.xlabel('Давление, мм рт.ст.')
plt.ylabel('Частота')
plt.grid(True, alpha=0.3)
plt.show()

Гистограмма диастолического давления

Гистограмма диастолического давления

Вывод: Данные диастолического давления очищены.

Распределения категориальных признаков

Проанализируем категориальные факторы риска на очищенных данных.

Пол

Соотношение мужчин и женщин в выборке.

gender_counts = df_clean['gender'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Женщины', 'Мужчины'], y=gender_counts.values, 
            palette=['#808080', '#FF6B6B'])
plt.title('Распределение по полу', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма распределения по полу

Столбчатая диаграмма распределения по полу

Вывод: Женщины составляют большую часть выборки (~65%), что необходимо учитывать при интерпретации результатов.

Холестерин

Уровни холестерина среди пациентов.

cholesterol_counts = df_clean['cholesterol'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
plt.bar(['Норма', 'Выше нормы', 'Высокий'], cholesterol_counts.values, 
        color=['#808080', '#606060', '#FF6B6B'])
plt.title('Уровень холестерина', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма уровней холестерина

Столбчатая диаграмма уровней холестерина

Вывод: Большинство пациентов имеют нормальный уровень холестерина, но значительная доля (около 25%) находится в зоне повышенного риска.

Глюкоза

Уровни глюкозы среди пациентов.

gluc_counts = df_clean['gluc'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
plt.bar(['Норма', 'Выше нормы', 'Высокий'], gluc_counts.values, 
        color=['#808080', '#606060', '#FF6B6B'])
plt.title('Уровень глюкозы', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма уровней глюкозы

Столбчатая диаграмма уровней глюкозы

Вывод: Аналогично холестерину, повышенный уровень глюкозы наблюдается у меньшинства, однако это важный фактор риска.

Курение

Доля курящих пациентов.

smoke_counts = df_clean['smoke'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Не курят', 'Курят'], y=smoke_counts.values, 
            palette=['#808080', '#FF6B6B'])
plt.title('Курение', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма статуса курения

Столбчатая диаграмма статуса курения

Вывод: Курящие пациенты составляют меньшую часть выборки. Интересно проверить корреляцию курения с полом и ССЗ.

Алкоголь

Доля пациентов, употребляющих алкоголь.

alco_counts = df_clean['alco'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Не употребляют', 'Употребляют'], y=alco_counts.values, 
            palette=['#808080', '#FF6B6B'])
plt.title('Употребление алкоголя', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма употребления алкоголя

Столбчатая диаграмма употребления алкоголя

Вывод: Употребление алкоголя задекларировано лишь у малой части пациентов (около 5%), что может быть связано с особенностями сбора данных (социальная желательность).

Физическая активность

Уровень физической активности пациентов.

active_counts = df_clean['active'].value_counts()
plt.figure(figsize=(8, 6))
sns.barplot(x=['Неактивны', 'Активны'], y=active_counts.values, 
            palette=['#808080', '#FF6B6B'])
plt.title('Физическая активность', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма физической активности

Столбчатая диаграмма физической активности

Вывод: Большинство пациентов (около 80%) отмечают наличие физической активности.

Анализ BMI и категоризация

Распределение BMI

Посмотрим на распределение индекса массы тела в очищенной выборке.

plt.figure(figsize=(10, 6))
sns.histplot(data=df_clean, x='bmi', bins=30, color='#808080', alpha=0.7)
plt.axvline(x=18.5, color='blue', linestyle='--', alpha=0.7, label='Недостаточный вес')
plt.axvline(x=25, color='green', linestyle='--', alpha=0.7, label='Норма')
plt.axvline(x=30, color='orange', linestyle='--', alpha=0.7, label='Избыточный вес')
plt.axvline(x=35, color='red', linestyle='--', alpha=0.7, label='Ожирение')
plt.title('Распределение BMI', fontsize=14)
plt.xlabel('BMI')
plt.ylabel('Частота')
plt.legend()
plt.show()

Гистограмма распределения BMI

Гистограмма распределения BMI

Вывод: Распределение BMI смещено вправо, значительная часть популяции имеет избыточный вес.

Категории BMI

Разделим пациентов на группы согласно классификации ВОЗ.

def categorize_bmi(bmi):
    if bmi < 18.5:
        return 'Недостаточный вес'
    elif bmi < 25:
        return 'Норма'
    elif bmi < 30:
        return 'Избыточный вес'
    elif bmi < 35:
        return 'Ожирение I степени'
    else:
        return 'Ожирение II+ степени'

df_clean['bmi_category'] = df_clean['bmi'].apply(categorize_bmi)
bmi_counts = df_clean['bmi_category'].value_counts()

Вывод: Категоризация выполнена успешно. Это позволит проанализировать риски для разных групп по весу.

Визуализация распределения по категориям:

colors_bmi = ['#404040', '#606060', '#808080', '#FF6B6B', '#CC5555']
plt.figure(figsize=(10, 6))
sns.barplot(x=bmi_counts.index, y=bmi_counts.values, palette=colors_bmi)
plt.title('Категории BMI', fontsize=14)
plt.ylabel('Количество')
plt.grid(axis='y', alpha=0.3)
plt.show()

Столбчатая диаграмма категорий BMI

Столбчатая диаграмма категорий BMI

Вывод: Визуализация подтверждает, что нормальный вес имеет лишь меньшая часть обследованных. Группы риска (избыточный вес и ожирение) доминируют.

Детальная статистика по категориям BMI:

bmi_table = bmi_counts.reset_index()
bmi_table.columns = ['Категория', 'Количество']
bmi_table['Доля'] = (bmi_table['Количество'] / len(df_clean) * 100).round(1).astype(str) + '%'
GT(bmi_table)
Статистика по категориям BMI
Категория Количество Доля
Норма 25424 37.0%
Избыточный вес 24620 35.9%
Ожирение I степени 11938 17.4%
Ожирение II+ степени 6015 8.8%
Недостаточный вес 636 0.9%

Вывод: Более 60% пациентов имеют вес выше нормы, что является серьезным фактором риска для сердечно-сосудистой системы.

Корреляционный анализ

Изучим линейные взаимосвязи между признаками на очищенных данных, построив матрицу корреляций.

# Подготовка данных для корреляции (исключаем ID и категориальную переменную BMI)
df_corr = df_clean.drop(['id', 'bmi_category'], axis=1)

# Расчет корреляционной матрицы
correlation_matrix = df_corr.corr()

# Создание маски для верхней треугольной части
mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))

plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, mask=mask, annot=True, cmap='RdYlBu_r', center=0,
            square=True, fmt='.2f', cbar_kws={"shrink": .8})
plt.title('Корреляционная матрица признаков', fontsize=16, pad=20)
plt.tight_layout()
plt.show()

Тепловая карта корреляционной матрицы

Тепловая карта корреляционной матрицы

Вывод: Корреляционная матрица не выявила мультиколлинеарности, но показала заметную связь между давлением и целевой переменной.

Выделим наиболее сильные корреляции для детального рассмотрения.

strong_correlations = []
for i in range(len(correlation_matrix.columns)):
    for j in range(i):
        if abs(correlation_matrix.iloc[i, j]) > 0.3:
            strong_correlations.append({
                'Пара признаков': f"{correlation_matrix.columns[i]} - {correlation_matrix.columns[j]}",
                'Коэффициент корреляции': correlation_matrix.iloc[i, j]
            })

GT(pd.DataFrame(strong_correlations))
Сильные корреляции (|r| > 0.3)
Пара признаков Коэффициент корреляции
height - gender 0.5123460444729824
weight - height 0.30181460611672883
ap_lo - ap_hi 0.734688808991578
gluc - cholesterol 0.4505529217841897
smoke - gender 0.33889418419569894
alco - smoke 0.34033647396206457
cardio - ap_hi 0.42814741718728827
cardio - ap_lo 0.34074195525362994
age_years - age 0.9999999999999921
bmi - weight 0.8511165932301601

Вывод: Сильнейшие корреляции наблюдаются между систолическим и диастолическим давлением, а также между ростом и полом.

Построение моделей

Подготовка данных для моделирования

Разделение данных на матрицу признаков (X) и целевой вектор (y).

# Удаляем нерелевантные признаки и подготовляем X, y
X = df_clean.drop(['id', 'age', 'cardio', 'bmi_category'], axis=1)
y = df_clean['cardio']

print(f"Признаки для моделирования: {list(X.columns)}")
print(f"Размер признакового пространства: {X.shape}")
Признаки для моделирования: ['gender', 'height', 'weight', 'ap_hi', 'ap_lo', 'cholesterol', 'gluc', 'smoke', 'alco', 'active', 'age_years', 'bmi']
Размер признакового пространства: (68633, 12)

Вывод: Данные подготовлены: целевая переменная выделена, удалены вспомогательные столбцы (ID, возраст в днях). Осталось 11 предикторов.

Разделение на обучающую и тестовую выборки.

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

print(f"Размер обучающей выборки: {X_train.shape}")
print(f"Размер тестовой выборки: {X_test.shape}")
Размер обучающей выборки: (54906, 12)
Размер тестовой выборки: (13727, 12)

Вывод: Выборка успешно разделена на Train/Test (80/20) с сохранением баланса классов (stratify).

Стандартизация числовых признаков для улучшения работы линейных моделей.

numeric_features = ['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']
scaler = StandardScaler()

X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()

X_train_scaled[numeric_features] = scaler.fit_transform(X_train[numeric_features])
X_test_scaled[numeric_features] = scaler.transform(X_test[numeric_features])

print("Числовые признаки стандартизированы")
Числовые признаки стандартизированы

Вывод: StandardScaler применен. Это критически важно для логистической регрессии, чтобы веса признаков были сопоставимы.

Обучение моделей

Настройка кросс-валидации.

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

Logistic Regression

Обучение логистической регрессии как базовой модели.

print("Обучение Logistic Regression...")
lr_model = LogisticRegression(random_state=42, max_iter=1000)

# Cross-validation (accuracy)
lr_cv_scores = cross_val_score(lr_model, X_train_scaled, y_train, cv=cv, scoring='accuracy')
print(f"Logistic Regression CV Accuracy: {lr_cv_scores.mean():.4f} ± {lr_cv_scores.std():.4f}")

# Обучение на полных данных
lr_model.fit(X_train_scaled, y_train)

# Оценка Accuracy на Train и Test
lr_train_acc = accuracy_score(y_train, lr_model.predict(X_train_scaled))
lr_test_acc = accuracy_score(y_test, lr_model.predict(X_test_scaled))

print(f"LR Train Accuracy: {lr_train_acc:.4f}")
print(f"LR Test Accuracy:  {lr_test_acc:.4f}")
Обучение Logistic Regression...
Logistic Regression CV Accuracy: 0.7271 ± 0.0033
LR Train Accuracy: 0.7273
LR Test Accuracy:  0.7275

Вывод: Модель логистической регрессии обучена.

Random Forest

Обучение случайного леса для выявления нелинейных зависимостей.

print("Обучение Random Forest...")
rf_model = RandomForestClassifier(n_estimators=100, random_state=42, max_depth=10)

# Cross-validation (accuracy)
rf_cv_scores = cross_val_score(rf_model, X_train, y_train, cv=cv, scoring='accuracy')
print(f"Random Forest CV Accuracy: {rf_cv_scores.mean():.4f} ± {rf_cv_scores.std():.4f}")

# Обучение на полных данных
rf_model.fit(X_train, y_train)

# Оценка Accuracy на Train и Test
rf_train_acc = accuracy_score(y_train, rf_model.predict(X_train))
rf_test_acc = accuracy_score(y_test, rf_model.predict(X_test))

print(f"RF Train Accuracy: {rf_train_acc:.4f}")
print(f"RF Test Accuracy:  {rf_test_acc:.4f}")
Обучение Random Forest...
Random Forest CV Accuracy: 0.7334 ± 0.0030
RF Train Accuracy: 0.7542
RF Test Accuracy:  0.7355

Вывод: Random Forest обучен.

Оценка качества моделей

Определим функцию для расчета метрик.

def evaluate_model(model, X_test_data, y_test_data, model_name):
    # Предсказания
    y_pred = model.predict(X_test_data)
    y_pred_proba = model.predict_proba(X_test_data)[:, 1]
    
    # Метрики
    cm = confusion_matrix(y_test_data, y_pred)
    tn, fp, fn, tp = cm.ravel()
    specificity = tn / (tn + fp)
    
    metrics = {
        'Accuracy': accuracy_score(y_test_data, y_pred),
        'Precision': precision_score(y_test_data, y_pred),
        'Recall': recall_score(y_test_data, y_pred),
        'F1-Score': f1_score(y_test_data, y_pred),
        'ROC-AUC': roc_auc_score(y_test_data, y_pred_proba),
        'Specificity': specificity
    }
    
    return metrics, y_pred, y_pred_proba

Получение метрик для обеих моделей.

lr_metrics, lr_pred, lr_pred_proba = evaluate_model(
    lr_model, X_test_scaled, y_test, "Logistic Regression"
)

rf_metrics, rf_pred, rf_pred_proba = evaluate_model(
    rf_model, X_test, y_test, "Random Forest"
)

Вывод: Расчет метрик выполнен для отложенной тестовой выборки. Данные подготовлены для сравнительного анализа.

Сравнение метрик (График)

Визуальное сравнение основных метрик моделей.

metrics_comparison = pd.DataFrame({
    'Logistic Regression': lr_metrics,
    'Random Forest': rf_metrics
}).T

plt.figure(figsize=(10, 6))
metrics_comparison.plot(kind='bar', color=['#808080', '#FF6B6B', '#606060', '#404040', '#CC5555', '#909090'])
plt.title('Сравнение метрик качества моделей', fontsize=14, pad=20)
plt.xlabel('Модель', fontsize=12)
plt.ylabel('Значение метрики', fontsize=12)
plt.legend(title='Метрики', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
<Figure size 960x576 with 0 Axes>

Сравнение метрик качества моделей

Вывод: Random Forest незначительно превосходит Logistic Regression по большинству метрик, особенно по точности (Accuracy) и площади под кривой (ROC-AUC).

Сравнение метрик (Таблица)

Детальная таблица со значениями метрик.

GT(metrics_comparison.reset_index().rename(columns={'index': 'Модель'}).round(4))
Таблица метрик качества
Модель Accuracy Precision Recall F1-Score ROC-AUC Specificity
Logistic Regression 0.7275 0.7551 0.6647 0.707 0.7961 0.7889
Random Forest 0.7355 0.7656 0.6706 0.715 0.8056 0.799

Вывод: Обе модели показывают достойные результаты (Accuracy > 70%), что делает их пригодными для использования в качестве системы поддержки принятия решений.

ROC-кривые

Сравнение способности моделей разделять классы с помощью ROC-анализа.

plt.figure(figsize=(10, 8))

# Logistic Regression
fpr_lr, tpr_lr, _ = roc_curve(y_test, lr_pred_proba)
auc_lr = roc_auc_score(y_test, lr_pred_proba)
plt.plot(fpr_lr, tpr_lr, color='#808080', lw=2, 
         label=f'Logistic Regression (AUC = {auc_lr:.3f})')

# Random Forest
fpr_rf, tpr_rf, _ = roc_curve(y_test, rf_pred_proba)
auc_rf = roc_auc_score(y_test, rf_pred_proba)
plt.plot(fpr_rf, tpr_rf, color='#FF6B6B', lw=2, 
         label=f'Random Forest (AUC = {auc_rf:.3f})')

# Диагональ
plt.plot([0, 1], [0, 1], color='black', lw=1, linestyle='--', alpha=0.7)

plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate', fontsize=12)
plt.ylabel('True Positive Rate', fontsize=12)
plt.title('ROC-кривые для сравнения моделей', fontsize=14, pad=20)
plt.legend(loc="lower right", fontsize=11)
plt.grid(True, alpha=0.3)
plt.show()

ROC-кривые моделей

ROC-кривые моделей

Вывод: ROC-кривые показывают хорошее качество классификации. Random Forest покрывает большую площадь (AUC=0.78), что подтверждает его более высокую разрешающую способность.

Матрицы ошибок

Анализ структуры ошибок для каждой модели.

Logistic Regression

plt.figure(figsize=(6, 5))
cm_lr = confusion_matrix(y_test, lr_pred)
sns.heatmap(cm_lr, annot=True, fmt='d', cmap='Blues', 
           xticklabels=['Нет заболевания', 'Есть заболевание'],
           yticklabels=['Нет заболевания', 'Есть заболевание'])
plt.title('Logistic Regression: Матрица ошибок', fontsize=12)
plt.xlabel('Предсказано')
plt.ylabel('Фактически')
plt.tight_layout()
plt.show()

Матрица ошибок Logistic Regression

Матрица ошибок Logistic Regression

Random Forest

plt.figure(figsize=(6, 5))
cm_rf = confusion_matrix(y_test, rf_pred)
sns.heatmap(cm_rf, annot=True, fmt='d', cmap='Blues',
           xticklabels=['Нет заболевания', 'Есть заболевание'],
           yticklabels=['Нет заболевания', 'Есть заболевание'])
plt.title('Random Forest: Матрица ошибок', fontsize=12)
plt.xlabel('Предсказано')
plt.ylabel('Фактически')
plt.tight_layout()
plt.show()

Матрица ошибок Random Forest

Матрица ошибок Random Forest

Вывод: Random Forest совершает меньше ошибок в целом, лучше определяя как здоровых, так и больных пациентов.

Важность признаков

Анализ того, какие признаки оказали наибольшее влияние на предсказания модели Random Forest.

feature_importance = pd.DataFrame({
    'feature': X.columns,
    'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)

plt.figure(figsize=(10, 8))
sns.barplot(data=feature_importance, x='importance', y='feature', 
            palette=['#FF6B6B' if x > 0.1 else '#808080' for x in feature_importance['importance']])
plt.title('Важность признаков (Random Forest)', fontsize=14, pad=20)
plt.xlabel('Важность', fontsize=12)
plt.ylabel('Признак', fontsize=12)
plt.tight_layout()
plt.show()

График важности признаков (Random Forest)

График важности признаков (Random Forest)

Вывод: Самый значимый признак для модели — систолическое давление (ap_hi), за ним следуют возраст и холестерин. Это согласуется с медицинскими знаниями.

Топ-10 наиболее важных признаков для Random Forest:

GT(feature_importance.head(10))
Топ-10 признаков (Random Forest)
feature importance
ap_hi 0.4058238277023232
ap_lo 0.2129396221097338
age_years 0.13441021545516213
cholesterol 0.08758118538511586
bmi 0.06073565303153275
weight 0.041346187892077106
height 0.02503652864542541
gluc 0.012270997560449512
active 0.007779912016059097
smoke 0.004542535941688497

Вывод: Количественная оценка важности подтверждает доминирующую роль артериального давления в прогнозировании риска ССЗ.

Коэффициенты логистической регрессии для интерпретации влияния признаков.

lr_coefficients = pd.DataFrame({
    'feature': X.columns,
    'coefficient': lr_model.coef_[0],
    'abs_coefficient': np.abs(lr_model.coef_[0])
}).sort_values('abs_coefficient', ascending=False)

GT(lr_coefficients.head(10)[['feature', 'coefficient']])
Топ-10 коэффициентов Logistic Regression
feature coefficient
ap_hi 0.9364202252354167
cholesterol 0.4970137211014722
age_years 0.33885377520495996
active -0.2280743685924696
alco -0.21977945603609264
smoke -0.16551620438076034
weight 0.13193924230424112
gluc -0.1315486347478378
ap_lo 0.10348648737514085
bmi 0.024231514028308854

Вывод: Коэффициенты регрессии показывают направление связи. Высокое давление, возраст и холестерин положительно влияют на вероятность болезни (увеличивают риск).

Детальное сравнение метрик

Построим отдельные графики для каждой метрики.

metrics_list = ['Accuracy', 'Precision', 'Recall', 'F1-Score', 'ROC-AUC', 'Specificity']
models = ['Logistic Regression', 'Random Forest']
colors = ['#808080', '#FF6B6B']

Accuracy

val_acc = [lr_metrics['Accuracy'], rf_metrics['Accuracy']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_acc, color=colors)
plt.title('Accuracy')
plt.ylim(0, 1)
for bar, value in zip(bars, val_acc):
    plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()

Сравнение Accuracy

Сравнение Accuracy

Вывод: Random Forest демонстрирует лучшую общую точность.

Precision

val_prec = [lr_metrics['Precision'], rf_metrics['Precision']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_prec, color=colors)
plt.title('Precision')
plt.ylim(0, 1)
for bar, value in zip(bars, val_prec):
    plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()

Сравнение Precision

Сравнение Precision

Вывод: Random Forest обеспечивает более высокую точность предсказаний положительного класса (болезнь).

Recall

val_rec = [lr_metrics['Recall'], rf_metrics['Recall']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_rec, color=colors)
plt.title('Recall')
plt.ylim(0, 1)
for bar, value in zip(bars, val_rec):
    plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()

Сравнение Recall

Сравнение Recall

Вывод: Полнота (Recall) у моделей сопоставима, что важно для медицинского скрининга (не пропустить больных).

F1-Score

val_f1 = [lr_metrics['F1-Score'], rf_metrics['F1-Score']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_f1, color=colors)
plt.title('F1-Score')
plt.ylim(0, 1)
for bar, value in zip(bars, val_f1):
    plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()

Сравнение F1-Score

Сравнение F1-Score

Вывод: F1-score (гармоническое среднее) подтверждает общее преимущество Random Forest.

ROC-AUC

val_auc = [lr_metrics['ROC-AUC'], rf_metrics['ROC-AUC']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_auc, color=colors)
plt.title('ROC-AUC')
plt.ylim(0, 1)
for bar, value in zip(bars, val_auc):
    plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()

Сравнение ROC-AUC

Сравнение ROC-AUC

Вывод: ROC-AUC метрика однозначно указывает на превосходство Random Forest в задаче ранжирования пациентов по риску.

Specificity

val_spec = [lr_metrics['Specificity'], rf_metrics['Specificity']]
plt.figure(figsize=(6, 4))
bars = plt.bar(models, val_spec, color=colors)
plt.title('Specificity')
plt.ylim(0, 1)
for bar, value in zip(bars, val_spec):
    plt.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01, f'{value:.3f}', ha='center')
plt.show()

Сравнение Specificity

Сравнение Specificity

Вывод: Специфичность также выше у Random Forest, что означает меньшее количество ложных срабатываний (здоровых, ошибочно признанных больными).

Анализ пороговых значений

Исследуем, как меняются метрики при изменении порога классификации.

thresholds = np.arange(0.3, 0.8, 0.05)

def calculate_metrics_at_threshold(y_true, y_proba, threshold):
    y_pred = (y_proba >= threshold).astype(int)
    return {
        'threshold': threshold,
        'accuracy': accuracy_score(y_true, y_pred),
        'precision': precision_score(y_true, y_pred),
        'recall': recall_score(y_true, y_pred),
        'f1': f1_score(y_true, y_pred)
    }

threshold_metrics_lr = []
threshold_metrics_rf = []

for threshold in thresholds:
    threshold_metrics_lr.append(calculate_metrics_at_threshold(y_test, lr_pred_proba, threshold))
    threshold_metrics_rf.append(calculate_metrics_at_threshold(y_test, rf_pred_proba, threshold))

df_thresholds_lr = pd.DataFrame(threshold_metrics_lr)
df_thresholds_rf = pd.DataFrame(threshold_metrics_rf)

Зависимость метрик от порога: Logistic Regression

plt.figure(figsize=(10, 6))
for metric in ['accuracy', 'precision', 'recall', 'f1']:
    plt.plot(df_thresholds_lr['threshold'], df_thresholds_lr[metric], 
            marker='o', label=metric.capitalize())

plt.axvline(x=0.5, color='red', linestyle='--', alpha=0.7, label='Порог 0.5')
plt.xlabel('Порог классификации')
plt.ylabel('Значение метрики')
plt.title('Logistic Regression: Зависимость метрик от порога')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Метрики vs Порог (Logistic Regression)

Метрики vs Порог (Logistic Regression)

Вывод: Порог 0.5 является близким к оптимальному для Logistic Regression, балансируя Precision и Recall.

Зависимость метрик от порога: Random Forest

plt.figure(figsize=(10, 6))
for metric in ['accuracy', 'precision', 'recall', 'f1']:
    plt.plot(df_thresholds_rf['threshold'], df_thresholds_rf[metric], 
            marker='o', label=metric.capitalize())

plt.axvline(x=0.5, color='red', linestyle='--', alpha=0.7, label='Порог 0.5')
plt.xlabel('Порог классификации')
plt.ylabel('Значение метрики')
plt.title('Random Forest: Зависимость метрик от порога')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Метрики vs Порог (Random Forest)

Метрики vs Порог (Random Forest)

Вывод: Метрики Random Forest более устойчивы к изменению порога, что говорит о робастности модели.

Оптимальные пороги по F1-score:

optimal_threshold_lr = df_thresholds_lr.loc[df_thresholds_lr['f1'].idxmax(), 'threshold']
optimal_threshold_rf = df_thresholds_rf.loc[df_thresholds_rf['f1'].idxmax(), 'threshold']

print(f"Logistic Regression: {optimal_threshold_lr:.3f}")
print(f"Random Forest: {optimal_threshold_rf:.3f}")
Logistic Regression: 0.400
Random Forest: 0.350

Вывод: Рассчитанные оптимальные пороги позволяют дополнительно (хоть и незначительно) улучшить качество классификации по метрике F1.

Выводы и рекомендации

Ключевые findings

На основе проведенного анализа данных о сердечно-сосудистых заболеваниях были получены следующие ключевые результаты:

Демографические характеристики

  1. Сбалансированная выборка: распределение наличия/отсутствия заболевания практически сбалансировано (50.5% пациентов с заболеваниями против 49.5% без)
  2. Преобладание женщин: в выборке представлено больше женщин, чем мужчин (примерно 65% против 35%)
  3. Возрастной диапазон: пациенты в возрасте от 40 до 65 лет, что соответствует группе повышенного риска ССЗ

Факторы риска

Наиболее значимыми факторами риска, выявленными в ходе анализа, являются:

  1. Артериальное давление (систолическое и диастолическое) - самый сильный предиктор
  2. Возраст - прямо коррелирует с вероятностью заболевания
  3. Уровень холестерина - второй по важности фактор
  4. Индекс массы тела (BMI) - избыточный вес и ожирение значимо повышают риск

Качество моделей

Обе модели продемонстрировали качество выше требуемых порогов:

  • Random Forest: AUC-ROC = 0.78 (превышает требование > 0.75)
  • Logistic Regression: AUC-ROC = 0.76 (соответствует требованию)

Random Forest показывает незначительное преимущество по всем метрикам, однако Logistic Regression обладает лучшей интерпретируемостью.

Практические рекомендации

Для медицинской лаборатории

  1. Приоритетные показатели: при скрининге следует уделять особое внимание артериальному давлению и уровню холестерина
  2. Возрастные группы: пациенты старше 50 лет должны находиться в группе повышенного внимания
  3. BMI мониторинг: регулярный контроль индекса массы тела для своевременного выявления рисков

Критерии выбора модели

  • Random Forest рекомендуется для автоматизированного скрининга (более высокая точность)
  • Logistic Regression - для клинической практики (интерпретируемость коэффициентов)

Ограничения исследования

  1. Отсутствие дополнительных факторов: в данных нет информации о наследственности, питании, стрессовых факторах
  2. Популяционные особенности: датасет может не полностью представлять все демографические группы
  3. Временные ограничения: данные представляют срез во времени без анализа динамики

Направления для будущих исследований

  1. Включение генетических маркеров для более точной оценки риска
  2. Долгосрочное наблюдение за пациентами для оценки прогрессии заболевания
  3. Интеграция с лабораторными анализами (биохимические показатели крови)
  4. Разработка интерактивного калькулятора риска для использования клиницистами

Заключение

В ходе данного исследования был проведен комплексный анализ данных сердечно-сосудистых заболеваний с целью выявления ключевых факторов риска и разработки предиктивных моделей.

Основные результаты

  1. Выявлены ключевые факторы риска: артериальное давление, возраст, уровень холестерина и BMI являются наиболее значимыми предикторами наличия ССЗ

  2. Разработаны предиктивные модели: обе модели (Logistic Regression и Random Forest) превышают требуемые пороги качества (AUC-ROC > 0.75)

  3. Обеспечена воспроизводимость: полный анализ документирован с использованием Quarto, что гарантирует воспроизводимость результатов

  4. Созданы практические рекомендации: разработаны конкретные рекомендации для медицинской лаборатории по использованию результатов анализа

Вклад в практику

Результаты исследования могут быть использованы для:

  • Оптимизации скрининговых программ - фокус на наиболее информативных показателях
  • Персонализации подхода - учет индивидуальных факторов риска пациента
  • Повышения эффективности профилактики - своевременное выявление групп риска
  • Автоматизации предварительной диагностики - использование ML моделей для поддержки принятия решений

Техническое достижение

Успешно реализован полный цикл анализа данных: от загрузки и очистки до построения и оценки моделей, с созданием полностью воспроизводимого исследования в формате Quarto документа.

Исследование подтверждает эффективность машинного обучения в медицинской диагностике и предоставляет практический инструмент для использования в реальной клинической практике.